#!/usr/bin/env python
# encoding: utf-8
"""
pyshD.py

Copyright (c) 2009 Thorsten Philipp (pyshD@kyri0s.de). All rights reserved.
"""

import sys
import getopt
import struct
import simplejson
from OpenSSL import SSL
from twisted.internet import reactor
from twisted.internet.protocol import ReconnectingClientFactory, ClientFactory, Protocol
from twisted.internet.ssl import ClientContextFactory
from twisted.python import log


class APNSClientContextFactory(ClientContextFactory):
    def __init__(self,certificate,pkey):
        self.ctx = SSL.Context(SSL.SSLv3_METHOD)
        self.ctx.use_certificate_file(certificate)
        self.ctx.use_privatekey_file(pkey)

    def getContext(self):
        return self.ctx

class APNSProtocol(Protocol):
    def connectionMade(self):
        log.msg ("Connection established.")
        self.factory.registerConnection(self)
        
        
    def sendMessage(self, deviceToken, payload):
        # notification messages are binary messages in network order
        # using the following format:
        # <1 byte command> <2 bytes length><token> <2 bytes length><payload>
        #deviceToken = "29d445f72b4b26431efb90c673f751af64ed1745fdba4b832e1f244651aacca0"
        #00002029d445f72b4b26431efb90c673f751af64ed1745fdba4b832e1f244651aaccc100267b22617073223a207b226261646765223a20352c2022616c657274223a202254657374227d7d
        if(len(payload)>256):
            log.err("Payload too long: %s " % payload)
            return
        if(len(deviceToken) != 64):
            log.err("Invalid Devicetoken: %s" % deviceToken)
        
        command = '\x00'
        msg = struct.pack("!cH",command,32)
        msg += deviceToken.decode("hex")
        msg += struct.pack("!H%ds" % len(payload), len(payload),payload)
        
        #log.msg("Sending data %s" % msg.encode("hex"))
        log.msg("#%s: [%s] - %s" % (self.factory.instance,deviceToken, payload))
        self.transport.write(msg)
        
    def dataReceived(self, data):
            log.err("Received data from Push Servers: %s" % data.encode("hex"))

class APNSClientFactory(ReconnectingClientFactory):
    initialDelay = .1
    protocol = APNSProtocol
    
    def __init__(self,service,instance=0):
        """service is expected to be an instance of APN.provider.provider"""
        self.service = service
        self.instance = instance
        self.service.registerPushfactory(self)
        self.__connection__ = None
        self.__messageQueue__ = []
        self.lastdevice = None
    
    def startedConnecting(self,connector):
        """docstring for startedConnecting"""
        log.msg("Started Connecting")
        
    def clientConnectionLost(self, connector, reason):
        print "Lost connection. Reason: %s" % reason
        self.__connection__  = None
        list(self.service.getDevices(deviceToken=self.lastdevice))[0].sendFailed()
        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
        
    def clientConnectionFailed(self, connector, reason):
        print "Connection failed. Reason: %s" % reason
        self.__connection__  = None
        ReconnectingClientFactory.clientConnectionFailed(self, connector,
                                                                 reason)
        
    def registerConnection(self,protocolObject):
        """Add a connection Object to __connections__"""
        self.__connection__ = protocolObject
        self.resetDelay()
        while len(self.__messageQueue__) > 0:
            # Some queued messages available
            deviceToken, payload = self.__messageQueue__.pop(0)
            self.sendMessage(deviceToken,payload)
        
    
    def sendMessage(self,deviceToken,payload):
        """docstring for sendMessage"""
        try:
            self.lastdevice = deviceToken
            self.__connection__.sendMessage(deviceToken,payload)
        except AttributeError:
            self.__messageQueue__.append((deviceToken,payload))


class APNSNotification(object):
    """Push Notification Payload Object
    alertbody: Alert Message
    localized: loc-key/ Key is treated as localizable string. Application knows correct text.
    button:    False/True/custom Value. None doesn't display the action button. False is Default Display. Custom is loc-key
    badge:     BadgeCount
    sound:     Play Soundfile (default: None) True Means the Default file is played.
    localizedArgs: A list of arguments to pass to localized
    
    Other Keyword parameters get appended with theire Value as Custom payload."""
    def __init__(self,alertbody=None,localized=None,actionButton=True,badge=None,sound=None,localizedArgs=[],**customJSON):
        super(APNSNotification, self).__init__()
        self.alertbody = alertbody
        self.localized = localized
        self.button = actionButton
        self.localizedArguments = localizedArgs
        self.badge = badge
        self.sound = sound
        self.customJSON = customJSON
        
    def asJSON(self):
        """Return the payload as a json string"""
        payload = {}
        payload["aps"] = {}
        
        # =========
        # = Alert =
        # =========
        if self.alertbody is not None:
            if (    (self.localized is None) and (self.button is True) and (self.localizedArguments is None)   ):
                payload["aps"]["alert"] = self.alertbody
            else:
                try:
                    payload["aps"]["alert"]["body"] = self.alertbody
                except KeyError:
                    payload["aps"]["alert"] = {}
                    payload["aps"]["alert"]["body"] = self.alertbody
                
        if self.localized is not None:
            try:
                payload["aps"]["alert"]["loc-key"] = self.localized
            except KeyError:
                payload["aps"]["alert"] = {}
                payload["aps"]["alert"]["loc-key"] = self.localized
        if len(self.localizedArguments) > 0 :
            try:
                payload["aps"]["alert"]["loc-args"] = self.localizedArguments
            except KeyError:
                payload["aps"]["alert"] = {}
                payload["aps"]["alert"]["loc-args"] = self.localizedArguments
        if self.button is not True:
            if self.button is False:
                self.button = None
            try:
                payload["aps"]["alert"]["action-loc-key"] = self.button
            except KeyError:
                payload["aps"]["alert"] = {}
                payload["aps"]["alert"]["action-loc-key"] = self.button
        for keyword in self.customJSON.keys():
            payload[keyword] = self.customJSON[keyword]
            
            
        # =========
        # = Badge =
        # =========
        if self.badge is not None:
            if self.badge != 0: #Empty payload for Badge means don't show a badge
                payload["aps"]["badge"] = int(self.badge)
                
        # =========
        # = Sound =
        # =========
        if self.sound is not None:
            if self.sound is True:
                payload["aps"]["sound"] = "default"
            else:
                payload["aps"]["sound"] = self.sound

        return simplejson.dumps(payload,separators=(',', ':'))
        
        
class FeedbackProtocol(Protocol):
    def connectionMade(self):
        log.msg("Feedback Service: Connection established")

    def dataReceived(self, data):
        self.log("FEEDBACK-DATA ..... : %s " % data.encode("hex"))
        try:
            (time,length,deviceToken) = struct.unpack("!LH32s",data)
        except struct.error:
            log.err("Invalid Data from Feedback Server: %s" % data.encode("hex"))
        return data.encode("hex")

class FEEDBACKClientFactory(ClientFactory):

    protocol = FeedbackProtocol

    def __init__(self,service,interval=180):
        """service is expected to be an instance of APN.provider.provider"""
        self.service = service
        self.interval = interval*60
        log.msg("Retry interval: %s" % self.interval)

    def clientConnectionLost(self, connector, reason):
        print "Lost connection. Reason: %s" % reason
        self.call = reactor.callLater(self.interval,connector.connect)

    def clientConnectionFailed(self, connector, reason):
        print "Connection closed. Reason: %s" % reason
        self.call = reactor.callLater(self.interval,connector.connect)
